/*
 * Created on 2006-02-18
 */
package dk.aiszone.lang.autosource.formatter;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import dk.aiszone.lang.autosource.generatorobjects.AbstractBody;
import dk.aiszone.lang.autosource.generatorobjects.Annotation;
import dk.aiszone.lang.autosource.generatorobjects.Argument;
import dk.aiszone.lang.autosource.generatorobjects.Class;
import dk.aiszone.lang.autosource.generatorobjects.ClassBody;
import dk.aiszone.lang.autosource.generatorobjects.Declaration;
import dk.aiszone.lang.autosource.generatorobjects.GenericClass;
import dk.aiszone.lang.autosource.generatorobjects.Import;
import dk.aiszone.lang.autosource.generatorobjects.InterfaceBody;
import dk.aiszone.lang.autosource.generatorobjects.JavaDoc;
import dk.aiszone.lang.autosource.generatorobjects.JavaFile;
import dk.aiszone.lang.autosource.generatorobjects.Method;
import dk.aiszone.lang.autosource.generatorobjects.MethodDeclaration;
import dk.aiszone.lang.autosource.generatorobjects.StaticArea;
import dk.aiszone.lang.autosource.generatorobjects.Variable;
import dk.aiszone.lang.autosource.generatorobjects.GenericClass.Operator;
import dk.aiszone.lang.autosource.generatorobjects.helpers.ImportsList;

/**
 * Can be used to dump contents to a string or file.
 * 
 * <pre>
 * FileWriter out = new FileWriter(file);
 * out.write(new JavaFormatterImpl().toJava(AbstractBody&lt;?&gt;));
 * out.close();
 * </pre>
 * 
 * @author <a href="mailto:ais@mail.dk">Ais</a>
 */
public class JavaFormatterImpl implements JavaFormatter {

    public String toJava(AbstractBody<?> body, int level) {
        if (body instanceof ClassBody) {
            return toJava((ClassBody)body, level);
        } else if (body instanceof InterfaceBody) {
            return toJava((InterfaceBody)body, level);
        }
        throw new IllegalArgumentException(body.getClass() + " not supported");
    }

    public String toJava(Annotation annotation, int level) {
        assert annotation.getType() != null : "type was null";

        StringBuilder sb = new StringBuilder();
        sb.append(Helper.getTabs(level)).append("@").append(toJava(annotation.getType()));
        if (!annotation.getValues().isEmpty()) {
            sb.append("(");
            for (Iterator<Entry<String, Object>> iterator = annotation.getValues().entrySet().iterator(); iterator.hasNext();) {
                Entry<String, Object> entry = iterator.next();
                if (entry.getValue() instanceof Annotation) {
                    sb.append(entry.getKey()).append("=").append(toJava((Annotation)entry.getValue(), 0));
                } else {
                    sb.append(entry.getKey()).append("=").append(entry.getValue());
                }
                if (iterator.hasNext()) {
                    sb.append(", ");
                }
            }
            sb.append(")");
        }

        return sb.toString();
    }

    public String toJava(Argument argument) {
        assert argument.getName() != null : "name was null";

        return toJava(argument.getType()) + " " + argument.getName();
    }

    public String toJava(Class _class) {
        assert _class.isVoid() || _class.isConstructor() || _class.getClassName() != null : "PackageName was null";

        if (_class.isVoid()) {
            return "void";
        } else if (_class.isConstructor()) {
            return "";
        }

        StringBuffer buf = new StringBuffer();

        String className = _class.getClassName();
        if (className != null) {
            buf.append(className);
        }

        if (_class.isArray()) {
            for (int i = 0; i < _class.getArrayDimensions(); i++) {
                buf.append("[]");
            }
        }

        if (_class.getGenerics() != null && !_class.getGenerics().isEmpty()) {
            buf.append("<");
            for (Iterator<GenericClass> iter = _class.getGenerics().iterator(); iter.hasNext();) {
                buf.append(toJava(iter.next()));
                if (iter.hasNext()) {
                    buf.append(", ");
                }
            }
            buf.append(">");
        }

        return buf.toString();
    }

    public String toJava(ClassBody body, int level) {
        assert body.getName() != null : "name was null";

        StringBuffer buf = new StringBuffer();

        if (body.getJavaDoc() != null) {
            buf.append(toJava(body.getJavaDoc(), level));
        }

        if (!body.getAnnotations().isEmpty()) {
            for (Annotation annotation : body.getAnnotations()) {
                buf.append(toJava(annotation, level)).append("\n");
            }
        }
        buf.append(Helper.getTabs(level));
        buf.append(toJava(body.getDeclaration()));
        buf.append("class ");
        buf.append(body.getName());

        List<GenericClass> generics = body.getGenerics();
        if (!generics.isEmpty()) {
            buf.append("<");
            for (Iterator<GenericClass> iter = generics.iterator(); iter.hasNext();) {
                buf.append(toJava(iter.next()));
                if (iter.hasNext()) {
                    buf.append(", ");
                }
            }
            buf.append(">");
        }

        Class extendsClass = body.getExtendsClass();
        if (extendsClass != null) {
            buf.append(" extends ");
            buf.append(toJava(extendsClass));
        }

        Set<Class> _implements = body.getImplements();
        if (!_implements.isEmpty()) {
            buf.append(" implements ");
            buf.append(toSeperatedList(_implements, ","));
        }
        buf.append(" {\n\n");

        level++;

        if (!body.getStaticAreas().isEmpty()) {
            for (Iterator<StaticArea> iterator = body.getStaticAreas().iterator(); iterator.hasNext();) {
                buf.append(toJava(iterator.next(), level)).append("\n");
            }
        }

        // Append declared variables
        Collection<Variable> variables = body.getVariables();
        if (!variables.isEmpty()) {
            for (Iterator<Variable> iter = variables.iterator(); iter.hasNext();) {
                buf.append(toJava(iter.next(), level)).append("\n");
            }
        }

        // Append constructors
        for (Iterator<Method> iter = body.getConstructors().iterator(); iter.hasNext();) {
            buf.append(toJava(iter.next(), level)).append("\n");
        }

        // Append methods
        List<Method> sortedMethods = body.getMethods();
        Collections.sort(sortedMethods);

        for (Iterator<Method> iter = sortedMethods.iterator(); iter.hasNext();) {
            buf.append(toJava(iter.next(), level)).append("\n");
        }

        // Append inner classes
        appendInnerClasses(body.getInnerClasses(), level, buf);

        level--;

        buf.append(Helper.getTabs(level) + "}");

        return buf.toString();
    }

    public String toJava(Declaration declaration) {
        StringBuffer res = new StringBuffer(declaration.getScope().toString());

        if (declaration.isAbstract()) {
            res.append("abstract ");
        }

        if (declaration.isTransient()) {
            res.append("transient ");
        }

        if (declaration.isStatic()) {
            res.append("static ");
        }

        if (declaration.isFinal()) {
            res.append("final ");
        }

        return res.toString();
    }

    public String toJava(GenericClass genericClass) {
        if (Operator.EXTENDS.equals(genericClass.getOperator())) {
            return genericClass.getIdentifier() + " extends " + toJava(genericClass.getType());
        } else if (Operator.SUPER.equals(genericClass.getOperator())) {
            return genericClass.getIdentifier() + " super " + toJava(genericClass.getType());
        }
        return toJava(genericClass.getType());
    }

    public String toJava(Import _import) {
        return "import " + _import.getPackageName() + ";";
    }

    public String toJava(ImportsList importsList) {
        Set<Import> imports = importsList.getImports();
        if (!imports.isEmpty()) {
            int oldValue = -1;
            StringBuffer buf = new StringBuffer();

            for (Iterator<Import> iter = imports.iterator(); iter.hasNext();) {
                Import _import = iter.next();

                if (oldValue != -1 && oldValue != _import.getOrderValue()) {
                    buf.append("\n");
                }
                buf.append(toJava(_import)).append("\n");

                oldValue = _import.getOrderValue();
            }

            buf.append("\n");

            return buf.toString();
        }
        return "";
    }

    public String toJava(InterfaceBody body, int level) {
        assert body.getName() != null : "name was null";

        StringBuffer buf = new StringBuffer();

        if (body.getJavaDoc() != null) {
            buf.append(toJava(body.getJavaDoc(), level)).append("\n");
        }

        buf.append(Helper.getTabs(level));
        buf.append(toJava(body.getDeclaration()));
        buf.append("interface ");
        buf.append(body.getName());

        List<Class> _implements = body.getExtends();
        if (!_implements.isEmpty()) {
            buf.append(" extends ");
            buf.append(toSeperatedList(_implements, ","));
        }
        buf.append(" {\n\n");

        level++;

        List<MethodDeclaration> sortedMethods = body.getMethodDeclarations();
        Collections.sort(sortedMethods);

        for (Iterator<MethodDeclaration> iter = sortedMethods.iterator(); iter.hasNext();) {
            buf.append(toJava(iter.next(), level)).append("\n");
        }

        appendInnerClasses(body.getInnerClasses(), level, buf);
        level--;

        buf.append(Helper.getTabs(level) + "}");

        return buf.toString();

    }

    public String toJava(JavaDoc javaDoc, int level) {
        StringBuffer buf = new StringBuffer();

        if (javaDoc.isEmpty()) {
            return "";
        }

        if (isSingleLined(javaDoc, true)) {
            buf.append(Helper.getTabs(level)).append("/** ").append(javaDoc.getDescription()).append(" */");
        } else {
            buf.append(Helper.getTabs(level)).append("/**\n");

            appendDescription(buf, javaDoc, level);

            appendSee(buf, javaDoc, level);

            if (javaDoc.getAuthor() != null) {
                buf.append(Helper.getTabs(level)).append(" * @author ").append(javaDoc.getAuthor()).append("\n");
            }
            if (javaDoc.getVersion() != null) {
                buf.append(Helper.getTabs(level)).append(" * @version ").append(javaDoc.getVersion()).append("\n");
            }
            if (javaDoc.getSince() != null) {
                buf.append(Helper.getTabs(level)).append(" * @since ").append(javaDoc.getSince()).append("\n");
            }

            Map<String, String> params = javaDoc.getParameters();
            if (!params.isEmpty()) {
                appendTag(buf, level, params.entrySet(), "param");
            }

            appendReturn(buf, javaDoc, level);

            Map<String, String> _throws = javaDoc.getThrows();
            if (!_throws.isEmpty()) {
                buf.append(Helper.getTabs(level)).append(" * \n");
                appendTag(buf, level, _throws.entrySet(), "throws");
            }

            buf.append(Helper.getTabs(level)).append(" */\n");
        }

        return buf.toString();
    }

    public String toJava(JavaFile javaFile) {
        StringBuffer buf = new StringBuffer(1000);

        buf.append(Helper.formatComment("created " + new SimpleDateFormat("MMM d, yyyy HH:mm").format(new Date())));

        buf.append("package ");
        buf.append(javaFile.getPackage());
        buf.append(";\n\n");

        buf.append(toJava(javaFile.getImports()));
        buf.append(toJava(javaFile.getMainClass(), 0));

        return buf.toString();
    }

    public String toJava(Method method, int level) {

        StringBuffer buf = new StringBuffer();

        appendDeclaredMethod(buf, method, level);

        if (method.getDeclaration().isAbstract()) {
            buf.append(";\n");
        } else {
            if (!method.isBodyEmpty()) {
                buf.append(" {\n");

                level++;

                buf.append(Helper.formatBody(method.getBodyParts(), level));

                level--;

                buf.append(Helper.getTabs(level) + "}\n");
            } else {
                buf.append(" {\n").append(Helper.getTabs(level)).append("// No implementation.\n").append(Helper.getTabs(level)).append("}\n");
            }
        }

        return buf.toString();

    }

    public String toJava(MethodDeclaration declaration, int level) {
        StringBuffer buf = new StringBuffer();
        appendDeclaredMethod(buf, declaration, level);
        return buf.append(";\n").toString();
    }

    public String toJava(StaticArea staticArea, int level) {
        StringBuffer java = new StringBuffer();

        java.append(Helper.getTabs(level)).append("static {\n");
        if (!staticArea.isBodyEmpty()) {
            level++;

            java.append(Helper.formatBody(staticArea.getBodyParts(), level));

            level--;
        }

        java.append(Helper.getTabs(level)).append("}\n");

        return java.toString();
    }

    public String toJava(Variable variable, int level) {
        StringBuffer buf = new StringBuffer();

        JavaDoc javaDoc = variable.getJavaDoc();
        if (javaDoc != null) {
            buf.append(toJava(javaDoc, level)).append("\n");
        }

        buf.append(Helper.getTabs(level));
        buf.append(toJava(variable.getDeclaration()));
        buf.append(toJava(variable.getType()));
        buf.append(" ");
        buf.append(variable.getName());

        String initialization = variable.getInitialization();
        if (initialization != null) {
            buf.append(" = ").append(initialization);
            if (!initialization.endsWith(";")) {
                buf.append(";");
            }
        } else {
            buf.append(";");
        }

        StaticArea staticArea = variable.getStaticArea();
        if (staticArea != null) {
            buf.append("\n").append(toJava(staticArea, level));
        }

        return buf.toString();
    }

    /**
     * Converts the main part of the <code>MethodDeclaration</code> to a <code>String</code>.<br>
     * The main parts covers the following:
     * <ul>
     * <li>The <code>JavaDoc</code>
     * <li>The <code>Declaration</code>
     * <li>The return type
     * <li>The name
     * <li>The arguments
     * <li>The exceptions
     * </ul>
     * 
     * @param buf
     *            <code>StringBuffer</code> to append java source to.
     * @param method
     *            <code>MethodDeclaration</code> to format.
     * @param level
     *            The tab level to indent the result with
     */
    private void appendDeclaredMethod(StringBuffer buf, MethodDeclaration method, int level) {
        Class returnType = method.getReturnType();

        assert method.getDeclaration() != null : "declaration was null";
        assert returnType != null : "returntype was null";
        assert method.getName() != null : "name was null";

        JavaDoc javaDoc = method.getJavaDoc();
        if (javaDoc != null) {
            buf.append(toJava(javaDoc, level));
        }

        if (!method.getAnnotations().isEmpty()) {
            for (Annotation annotation : method.getAnnotations()) {
                buf.append(toJava(annotation, level)).append("\n");
            }
        }

        buf.append(Helper.getTabs(level));
        buf.append(toJava(method.getDeclaration()));

        if (!Class.CONSTRUCTOR.equals(returnType)) {
            buf.append(toJava(returnType)).append(" ");
        }

        buf.append(method.getName());

        // Append arguments
        buf.append("(");
        for (Iterator<Argument> iter = method.getArguments().iterator(); iter.hasNext();) {
            buf.append(toJava(iter.next()));

            if (iter.hasNext()) {
                buf.append(", ");
            }
        }
        buf.append(")");

        // Append exception
        Set<Class> exceptions = method.getExceptions();
        if (!exceptions.isEmpty()) {
            buf.append(" throws ");
            for (Iterator<Class> iter = exceptions.iterator(); iter.hasNext();) {
                buf.append(toJava(iter.next()));

                if (iter.hasNext()) {
                    buf.append(", ");
                }
            }
        }
    }

    private void appendDescription(StringBuffer buf, JavaDoc javaDoc, int level) {
        String description = javaDoc.getDescription();
        if (description != null) {
            List<String> parts = splitJavaDoc(description);
            for (Iterator<String> iterator = parts.iterator(); iterator.hasNext();) {
                String line = iterator.next();

                buf.append(Helper.getTabs(level)).append(" * ").append(line.trim());
                if (iterator.hasNext()) {
                    buf.append("<br>\n");
                } else {
                    buf.append("\n");
                }

            }

            if (!isSingleLined(javaDoc, false)) {
                buf.append(Helper.getTabs(level)).append(" * \n");
            }
        }
    }

    private void appendInnerClasses(List<AbstractBody<? extends MethodDeclaration>> innerClasses, int level, StringBuffer buf) {
        List<AbstractBody<? extends MethodDeclaration>> sortedInnerclasses = new ArrayList<AbstractBody<? extends MethodDeclaration>>(innerClasses);

        Collections.sort(sortedInnerclasses);
        for (Iterator<? extends AbstractBody<?>> iter = sortedInnerclasses.iterator(); iter.hasNext();) {
            AbstractBody<?> _body = iter.next();

            buf.append(toJava(_body, level)).append("\n");

            if (iter.hasNext()) {
                buf.append("\n");
            }
        }
    }

    private void appendReturn(StringBuffer buf, JavaDoc javaDoc, int level) {
        String _return = javaDoc.getReturn();

        if (_return != null) {
            buf.append(Helper.getTabs(level)).append(" * @return ");

            List<String> parts = splitJavaDoc(_return);
            for (Iterator<String> iterator = parts.iterator(); iterator.hasNext();) {
                buf.append(iterator.next().trim());

                if (iterator.hasNext()) {
                    buf.append("<br>\n").append(Helper.getTabs(level)).append(" *         ");
                } else {
                    buf.append("\n");
                }
            }
        }
    }

    private void appendSee(StringBuffer buf, JavaDoc javaDoc, int level) {
        Map<String, String> sees = javaDoc.getSees();

        if (!sees.isEmpty()) {
            for (Iterator<Map.Entry<String, String>> iter = sees.entrySet().iterator(); iter.hasNext();) {
                Map.Entry<String, String> entry = iter.next();

                buf.append(Helper.getTabs(level)).append(" * @see ").append(entry.getKey());
                if (entry.getValue() != null && entry.getValue().length() > 0) {
                    buf.append(" ").append(entry.getValue());
                }
                buf.append("\n");
            }

            buf.append(Helper.getTabs(level)).append(" * \n");
        }
    }

    private void appendTag(StringBuffer javadoc, int level, Set<Map.Entry<String, String>> entries, String tag) {
        for (Iterator<Map.Entry<String, String>> iter = entries.iterator(); iter.hasNext();) {
            Map.Entry<String, String> entry = iter.next();

            if (entry.getValue() != null) {
                javadoc.append(Helper.getTabs(level)).append(" * @").append(tag).append(" ").append(entry.getKey()).append("\n");

                List<String> parts = splitJavaDoc(entry.getValue());
                for (Iterator<String> iterator = parts.iterator(); iterator.hasNext();) {
                    javadoc.append(Helper.getTabs(level)).append(" *            ").append(iterator.next().trim());

                    if (iterator.hasNext()) {
                        javadoc.append("<br>\n");
                    } else {
                        javadoc.append("\n");
                    }
                }
            }
        }
    }

    private int indexOf(String string, int startIndex) {
        int indexOfNewLine = string.indexOf('\n', startIndex);
        int indexOfHtmlNewLine = string.indexOf("<br>", startIndex);
        if (indexOfNewLine == -1) {
            return indexOfHtmlNewLine;
        } else if (indexOfHtmlNewLine == -1) {
            return indexOfNewLine;
        }
        return (indexOfNewLine < indexOfHtmlNewLine ? indexOfNewLine : indexOfHtmlNewLine);
    }

    private boolean isSingleLined(JavaDoc javaDoc, boolean includeDescription) {
        if (javaDoc.getAuthor() != null) {
            return false;
        } else if (!javaDoc.getParameters().isEmpty()) {
            return false;
        } else if (javaDoc.getReturn() != null) {
            return false;
        } else if (!javaDoc.getSees().isEmpty()) {
            return false;
        } else if (javaDoc.getSince() != null) {
            return false;
        } else if (!javaDoc.getThrows().isEmpty()) {
            return false;
        } else if (javaDoc.getVersion() != null) {
            return false;
        } else if (includeDescription && javaDoc.getDescription() != null) {
            String description = javaDoc.getDescription();
            if (description.indexOf('\n') > -1) {
                return false;
            } else if (description.indexOf("<br>") > -1) {
                return false;
            }
        }

        return true;
    }

    private List<String> splitJavaDoc(String string) {
        List<String> lines = new ArrayList<String>();

        int startIndex = 0;
        int indexof = indexOf(string, startIndex);
        while (indexof > 0) {
            lines.add(string.substring(startIndex, indexof));

            if (string.charAt(indexof) == '<') {
                startIndex = indexof + 4;
            } else {
                startIndex = indexof + 1;
            }
            indexof = indexOf(string, startIndex);
        }
        lines.add(string.substring(startIndex));

        return lines;
    }

    /**
     * Converts the supplied <code>Collection</code> of elements to a <code>String</code>.<br>
     * Where each element is seperated with the supplied <code>seperator</code>.
     * 
     * @param elements
     *            <code>Collection</code> of element to convert to a <code>String</code>.
     * @param seperator
     *            The seperator to seperate each element with.
     * @return <code>String</code> of elements.
     */
    private String toSeperatedList(Collection<Class> elements, String seperator) {
        StringBuffer buf = new StringBuffer();

        for (Iterator<Class> iter = elements.iterator(); iter.hasNext();) {
            buf.append(toJava(iter.next()));

            if (iter.hasNext()) {
                buf.append(seperator).append(" ");
            }
        }
        return buf.toString();
    }
}